// Copyright 2011, Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.api.ads.common.lib.auth;

import com.google.api.ads.common.lib.conf.ConfigurationHelper;
import com.google.api.ads.common.lib.conf.ConfigurationLoadException;
import com.google.api.ads.common.lib.exception.ClientLoginException;
import com.google.api.ads.common.lib.exception.ValidationException;
import com.google.api.ads.common.lib.utils.Streams;
import com.google.api.client.googleapis.auth.clientlogin.ClientLoginResponseException;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;

import org.apache.commons.configuration.Configuration;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

/**
 * ClientLogin token provider.<br>
 * <br>
 * Example usage:
 * <pre><code>
 * ClientLoginTokens clientLoginTokens = new ClientLoginTokens.Builder()
 *     .forApi(ClientLoginTokens.Api.ADWORDS)
 *     .fromFile()
 *     .build();
 * String clientLoginToken = clientLoginTokens.requestToken();</code></pre>
 * If you receive a CAPTCHA challenge you can supply the login token and captcha
 * in the requestToken function like:
 * <pre><code>
 * String clientLoginToken;
 * try {
 *   clientLoginToken = clientLoginTokens.requestToken();
 * } catch(ClientLoginException e) {
 *   if (e.getCaptchaInfo != null) {
 *      String loginCaptcha =
 *          // redirect user to e.getCaptchaInfo.getCaptchaUrl() to get CAPTCHA;
 *      String loginToken = captchaInformation.getCaptchaToken();
 *      clientLoginToken =
 *          clientLoginTokens.requestToken(loginCaptcha, loginToken);
 *   }
 * }</code></pre>
 * @author Adam Rogal
 */
public class ClientLoginTokens {

  /**
   * Service to use to request ClientLogin tokens for AdWords.
   * @deprecated Use {@link Api#ADWORDS} in {@code Builder#forApi(Api)}.
   */
  @Deprecated
  public static final String ADWORDS_SERVICE = "adwords";
  /**
   * Service to use to request ClientLogin tokens for DFP.
   * @deprecated Use {@link Api#DFP} in {@code Builder#forApi(Api)}.
   */
  @Deprecated
  public static final String DFP_SERVICE = "gam";

  /**
   * Enum representing the API that ClientLoginTokens can be used for.
   */
  public static enum Api {
    ADWORDS("adwords", "api.adwords."), DFP("gam", "api.dfp.");

    private final String service;
    private final String propKeyPrefix;

    private Api(String service, String propKeyPrefix) {
      this.service = service;
      this.propKeyPrefix = propKeyPrefix;
    }

    /**
     * Gets the service.
     */
    public String getService() {
      return service;
    }

    /**
     * Gets the property key prefix.
     */
    public String getPropKeyPrefix() {
      return propKeyPrefix;
    }
  }

  @Deprecated
  private static final String ADWORDS_PROP_KEY_PREFIX = "api.adwords.";
  @Deprecated
  private static final String DFP_PROP_KEY_PREFIX = "api.dfp.";
  @Deprecated
  private static final String RESOURCE_PROPERTIES_NAME = "ads.properties";

  private static final int HTTP_CLIENT_SOCKET_TIMEOUT_IN_MS = (int) TimeUnit.SECONDS.toMillis(20);
  private static final String AUTH_TOKEN_KEY = "Auth";
  private static final String ERROR_KEY = "Error";
  private static final String URL_KEY = "Url";
  private static final String CAPTCHA_TOKEN_KEY = "CaptchaToken";
  private static final String CAPTCHA_URL_KEY = "CaptchaUrl";
  private static final String CAPTCHA_REQUIRED_ERROR = "CaptchaRequired";
  private static final String DEFAULT_CLIENT_LOGIN_SERVER_URL =
      "https://www.google.com/accounts/ClientLogin";
  private static final String INFO_KEY = "Info";
  private static final String SOURCE = "Ads-Api-Java-Lib";

  @Deprecated
  private String service;
  @Deprecated
  private ConfigurationHelper configHelper;
  @Deprecated
  private String clientLoginServerUrl = DEFAULT_CLIENT_LOGIN_SERVER_URL;

  @VisibleForTesting
  ClientLogin clientLogin;
  private String clientLoginTokenOverride;

  /**
   * Adds the correct property key prefix to match the provided service.
   * @param suffix
   * @return property value for key
   */
  @VisibleForTesting
  @Deprecated
  String getPropertyKey(String suffix) {
    if (ADWORDS_SERVICE.equalsIgnoreCase(this.service)) {
      return ADWORDS_PROP_KEY_PREFIX + suffix;
    } else if (DFP_SERVICE.equalsIgnoreCase(this.service)) {
      return DFP_PROP_KEY_PREFIX + suffix;
    }

    return suffix;
  }

  /**
   * Constructor.
   */
  @VisibleForTesting
  ClientLoginTokens(ClientLogin clientLogin) {
    this.clientLogin = clientLogin;
  }

  /**
   * Constructor.
   *
   * @param service the service name i.e. gam or adwords
   *
   * @deprecated Use <code>new ClientLoginTokens.Builder()</code> instead
   */
  @Deprecated
  public ClientLoginTokens(String service) {
    this(service, new ConfigurationHelper());
  }


  private ClientLoginTokens(String service, ConfigurationHelper helper) {
    this.service = service;
    this.configHelper = helper;
  }

  /**
   * Loads credentials from a properties file in one of the following
   * locations according to the following order: in the current directory, in
   * the user home directory, in the classpath
   *
   * @throws ConfigurationLoadException if the configuration could not be
   *         loaded.
   *
   * @deprecated Use <code>new ClientTokens.Builder().forApi(Api.ADWORDS)
   *             .fromFile()</code> instead.
   */
  @Deprecated
  public String fromFile() throws ClientLoginException, ConfigurationLoadException {
    return fromFile(RESOURCE_PROPERTIES_NAME);
  }

  /**
   * Populates this builder from a properties file from a specified path. If
   * not absolute, will look in user home directory, the current classpath and
   * the system classpath.
   *
   * @param path the path to try first as a resource, then as a file
   * @return Builder populated with properties.
   * @throws ConfigurationLoadException if the configuration could not be
   *         loaded.
   *
   * @deprecated Use <code>new ClientTokens.Builder().forApi(Api.ADWORDS)
   *             .fromFile(path)</code> instead.
   */
  @Deprecated
  public String fromFile(String path) throws ClientLoginException, ConfigurationLoadException {
    return from(configHelper.fromFile(path));
  }

  /**
   * Populates this builder from a properties file from a specified path.
   *
   * @param path the path to load properties from
   * @return Builder populated with properties.
   * @throws ConfigurationLoadException if the configuration could not be
   *         loaded.
   *
   * @deprecated Use <code>new ClientTokens.Builder().forApi(Api.ADWORDS)
   *             .fromFile(path)</code> instead.
   */
  @Deprecated
  public String fromFile(File path) throws ClientLoginException, ConfigurationLoadException {
    return from(configHelper.fromFile(path));
  }

  /**
   * Populates this builder from a properties file from a specified path. If
   * not absolute, will look in user home directory, the current classpath and
   * the system classpath.
   *
   * @param path the path to try first as a resource, then as a file
   * @return Builder populated with properties.
   * @throws ConfigurationLoadException if the configuration could not be
   *         loaded.
   *
   * @deprecated Use <code>new ClientTokens.Builder().forApi(Api.ADWORDS)
   *             .fromFile(path)</code> instead.
   */
  @Deprecated
  public String fromFile(URL path) throws ClientLoginException, ConfigurationLoadException {
    return from(configHelper.fromFile(path));
  }

  /**
   * Obtains a ClientLoginToken either stored in the specified configuration or
   * using an email and password stored in the configuration to obtain a new one
   *
   * <br>
   * <br>
   * Understands the following properties suffixes:
   * <ul>
   * <li>clientLoginToken</li>
   * <li>email</li>
   * <li>password</li>
   * </ul>
   * For example, the AdWords ClientLoginToken can be read from:
   * <code>api.adwords.clientLoginToken</code>
   *
   * @param config
   * @return ClientLoginToken on success or an exception.
   * @throws ClientLoginException
   *
   * @deprecated Use <code>new ClientTokens.Builder().forApi(Api.ADWORDS)
   *             .from(config) </code> instead.
   */
  @Deprecated
  public String from(Configuration config) throws ClientLoginException {
    if (config.getString(getPropertyKey("clientLoginToken")) != null) {
      return config.getString(getPropertyKey("clientLoginToken"));
    }
    if (config.getString(getPropertyKey("email")) != null
        && config.getString(getPropertyKey("password")) != null) {
      return from(config.getString(getPropertyKey("email")),
          config.getString(getPropertyKey("password")));
    }
    throw new IllegalArgumentException(
        "Expected Configuration to contain either a clientLoginToken or an email and password.");
  }

  /**
   * Requests the user's authentication token.
   *
   * @param email the email to fetch the token for
   * @param password the password to fetch the token for
   * @return the user's new authentication token
   * @throws ClientLoginException if the token could not be generated
   *
   * @deprecated Use <code>new ClientTokens.Builder().forApi(Api.ADWORDS)
   *             .withEmailAndPassword(email, password)</code> instead.
   */
  @Deprecated
  public String from(String email, String password) throws ClientLoginException {
    return from(email, password, null, null);
  }

  /**
   * Requests the user's authentication token.
   *
   * @param email the email to fetch the token for
   * @param password the password to fetch the token for
   * @param loginToken the login token. Can be {@code null} if not needed.
   * @param loginCaptcha the login CAPTCHA. Can be {@code null} if not needed.
   * @return the user's new authentication token
   * @throws ClientLoginException if the token could not be generated
   *
   * @deprecated Use <code>new ClientTokens.Builder()
   *             .forApi(Api.ADWORDS)
   *             .withEmailAndPassword(email, password)
   *             .withLoginTokenAndCaptcha(loginToken,
   *             loginCaptcha)</code> instead.
   */
  @Deprecated
  public String from(String email, String password, String loginToken, String loginCaptcha)
      throws ClientLoginException {
    return deprecatedRequestToken(email, password, loginToken, loginCaptcha);
  }

  /**
   * Requests a token with the email and password, along with information used
   * to validate a CAPTCHA'ed account.
   *
   * @param captchaToken the CAPTCHA token. i.e.
   *        {@link com.google.api.client.googleapis.auth.clientlogin
   *               .ClientLogin.ErrorInfo#captchaToken ErrorInfo#captchaToken}
   * @param captchaAnswer the CAPTCHA answer. i.e. fetched from
   *        {@link com.google.api.client.googleapis.auth.clientlogin
   *        .ClientLogin.ErrorInfo#captchaUrl ErrorInfo#captchaUrl}
   * @return the client login token
   * @throws ClientLoginResponseException if the token could not be retrieved
   * @throws IOException if there was a problem making the request
   */
  public String requestToken(String captchaToken, String captchaAnswer)
      throws ClientLoginResponseException, IOException {
    if (clientLogin == null) {
      throw new IllegalStateException(
          "requestToken() and requestToken(captchaToken, captchaAnswer) can only be used with " +
          "ClientLoginTokens created from ClientLogin.Builder");
    }
    try {
      clientLogin.setCaptchaAnswer(captchaAnswer);
      clientLogin.setCaptchaToken(captchaToken);
      return requestToken();
    } finally {
      clientLogin.setCaptchaAnswer(null);
      clientLogin.setCaptchaToken(null);
    }
  }

  /**
   * Checks to see if we can use an existing clientLoginToken from file,
   * otherwise delegates to {@link ClientLoginTokens#requestToken()}.
   *
   * @return the client login token
   * @throws ClientLoginResponseException if the token could not be retrieved,
   *         possibly due to a {@link ClientLoginResponseException#getDetails()
   *         CAPTCHA} challenge
   * @throws IOException if there was a problem making the request
   */
  public String getToken() throws ClientLoginResponseException, IOException {
    return (clientLoginTokenOverride != null && clientLoginTokenOverride.trim().length() > 0) ?
        clientLoginTokenOverride : requestToken();
  }

  /**
   * Requests a token with the email and password.
   *
   * @return the client login token
   * @throws ClientLoginResponseException if the token could not be retrieved, possibly
   *         due to a {@link ClientLoginResponseException#getDetails() CAPTCHA}
   *         challenge
   * @throws IOException if there was a problem making the request
   */
  public String requestToken() throws ClientLoginResponseException, IOException {
    if (clientLogin == null) {
      throw new IllegalStateException(
          "requestToken() and requestToken(captchaToken, captchaAnswer) can only be used with " +
          "ClientLoginTokens created from ClientLogin.Builder");
    }
    return clientLogin.requestToken();
  }

  /**
   * Requests a token with the login token and CAPTCHA if provided.
   *
   * @param email the user's email
   * @param password the user's password
   * @param loginToken the login token. Can be {@code null} if not needed.
   * @param loginCaptcha the login CAPTCHA. Can be {@code null} if not needed.
   * @return the ClientLogin token
   * @throws ClientLoginException if the token could not be generated
   *
   * @deprecated To be removed in 1.1
   */
  @Deprecated
  private String deprecatedRequestToken(String email, String password,
      String loginToken, String loginCaptcha) throws ClientLoginException {
    if (!Strings.isNullOrEmpty(clientLoginTokenOverride)) {
      return clientLoginTokenOverride;
    }
    try {
      HttpURLConnection conn = postToClientLogin(email, password, loginToken, loginCaptcha);
      int statusCode = conn.getResponseCode();
      String responseBody =
          Streams.readAll(statusCode == HttpURLConnection.HTTP_OK ? conn
              .getInputStream() : conn.getErrorStream());
      Properties responseProperties =
          generatePropertiesFromResponse(Streams.wrapString(responseBody));

      if (statusCode == HttpURLConnection.HTTP_OK) {
        if (responseProperties.containsKey(AUTH_TOKEN_KEY)) {
          return responseProperties.getProperty(AUTH_TOKEN_KEY).toString();
        } else {
          throw new IllegalStateException("Unable to get auth token from Client Login server");
        }
      } else {
        CaptchaInformation captchaInfo = null;
        String errorCode = null;

        if (responseProperties.containsKey(ERROR_KEY)) {
          errorCode = responseProperties.getProperty(ERROR_KEY);
          if (errorCode != null && errorCode.equals(CAPTCHA_REQUIRED_ERROR)) {
            captchaInfo = extractCaptchaInfoFromProperties(responseProperties);
          }
          if (responseProperties.getProperty(INFO_KEY) != null) {
            errorCode = errorCode + ": " + responseProperties.getProperty(INFO_KEY);
          }
        }

        throw new ClientLoginException(statusCode, responseBody, errorCode,
            captchaInfo, null);
      }
    } catch (IOException e) {
      throw new ClientLoginException(null, null, null, null, e);
    }
  }

  /**
   * Makes the POST to the client login API.
   *
   * @param email the email
   * @param password the password
   * @param loginToken the login token or {@code null} if not needed
   * @param loginCaptcha the login CAPTCHA or {@code null} if not needed
   * @return HttpUrlConnection with request made and ready for response to be
   *         obtained.
   * @throws IOException if the HTTP client could not establish a
   *     connection
   * @deprecated Will be removed when
   *             {@link #deprecatedRequestToken(String, String, String, String)}
   *             is
   */
  @Deprecated
  private HttpURLConnection postToClientLogin(String email, String password,
      String loginToken, String loginCaptcha) throws IOException {
    HttpURLConnection conn = getConnection("POST");
    Map<String, String> params = new TreeMap<String, String>();
    params.put("Email", email);
    params.put("Passwd", password);
    params.put("accountType", "GOOGLE");
    params.put("service", service);
    params.put("source", SOURCE);

    if (loginToken != null) {
      params.put("logintoken", loginToken);
    }

    if (loginCaptcha != null) {
      params.put("logincaptcha", loginCaptcha);
    }

    String payload = generateParamsPayload(params);
    Streams.write(payload, conn.getOutputStream());

    conn.connect();
    return conn;
  }

  /**
   * Configures an {@link HttpURLConnection}.
   *
   * @param requestMethod Request method for this connection.  If POST,
   *        conn.setDoOutput() is set to true.
   * @returns Configured connection.
   * @throws IOException if we cannot configure the connection as requested.
   * @deprecated Will be removed when
   *             {@link #deprecatedRequestToken(String, String, String, String)}
   *             is
   */
  @Deprecated
  private HttpURLConnection getConnection(String requestMethod)
      throws IOException {
    HttpURLConnection conn = (HttpURLConnection) new URL(clientLoginServerUrl).openConnection();
    conn.setConnectTimeout(HTTP_CLIENT_SOCKET_TIMEOUT_IN_MS);
    conn.setRequestMethod(requestMethod);
    if ("POST".equalsIgnoreCase(requestMethod)) {
      conn.setDoOutput(true);
    }
    return conn;
  }

  /**
   * Transforms map of key value mappings to an encoded query string.
   *
   * @param params Map of all parameters to send to the service.
   * @deprecated Will be removed when
   *             {@link #deprecatedRequestToken(String, String, String, String)}
   *             is
   */
  @Deprecated
  private String generateParamsPayload(Map<String, String> params) {
    StringBuilder sb = new StringBuilder();
    for (Map.Entry<String, String> entry : params.entrySet()) {
      if (sb.length() > 0) {
        sb.append("&");
      }
      try {
        sb.append(entry.getKey()).append("=").append(URLEncoder.encode(entry.getValue(), "UTF-8"));
      } catch (UnsupportedEncodingException e) {
        throw new IllegalStateException("UTF-8 should be a supported encoding.", e);
      }
    }
    return sb.toString();
  }

  /**
   * Extracts the CAPTCHA information from a response.
   *
   * @param authProperties the response from the client login API
   * @return the CAPTCHA information
   * @deprecated Will be removed when
   *             {@link #deprecatedRequestToken(String, String, String, String)}
   *             is
   */
  @Deprecated
  private CaptchaInformation extractCaptchaInfoFromProperties(Properties authProperties) {
    String captchaUrl = clientLoginServerUrl + authProperties.getProperty(CAPTCHA_URL_KEY);
    String captchaToken = authProperties.getProperty(CAPTCHA_TOKEN_KEY);
    String url = authProperties.getProperty(URL_KEY);
    return new CaptchaInformation(captchaUrl, captchaToken, url);
  }

  /**
   * Generates a {@code Properties} from a client login API response
   *
   * @param responseBodyStream the response body as a stream
   * @return a {@code Properties} generated from the response body
   * @throws IOException if response body stream could not be read
   * @deprecated Will be removed when
   *             {@link #deprecatedRequestToken(String, String, String, String)}
   *             is
   */
  @Deprecated
  private static Properties generatePropertiesFromResponse(InputStream responseBodyStream)
      throws IOException {
    Properties properties = new Properties();
    properties.load(responseBodyStream);
    return properties;
  }

  /**
   * Pre-builder for ClientLoginTokens.
   */
  public static class Builder {

    private final ForApiBuilder forApiBuilder;

    /**
     * Constructor.
     */
    public Builder() {
      this.forApiBuilder = new ForApiBuilder();
    }

    /**
     * Specifies which {@link Api} should this {@code ClientLoginTokens} be used
     * for. Should be called first before any other builder methods.
     */
    public ForApiBuilder forApi(Api api) {
      forApiBuilder.api = api;
      return forApiBuilder;
    }
  }

  /**
   * Builder for ClientLoginTokens.
   */
  public static class ForApiBuilder implements
      com.google.api.ads.common.lib.utils.Builder<ClientLoginTokens> {

    // Use thread-safe global instance as default.
    private static final HttpTransport DEFAULT_HTTP_TRANSPORT = new NetHttpTransport();

    @VisibleForTesting
    final ConfigurationHelper configHelper;
    private final ClientLoginTokens clientLoginTokens;

    @VisibleForTesting
    Api api;

    /**
     * Private constructor.
     */
    private ForApiBuilder() {
      this(new ConfigurationHelper(), new ClientLogin());
    }

    @VisibleForTesting
    ForApiBuilder(ConfigurationHelper configHelper, ClientLogin clientLogin) {
      this.configHelper = configHelper;
      clientLoginTokens = new ClientLoginTokens(clientLogin);
    }

    public ForApiBuilder fromFile(String path) throws ConfigurationLoadException {
      return from(configHelper.fromFile(path));
    }

    public ForApiBuilder fromFile(File path) throws ConfigurationLoadException {
      return from(configHelper.fromFile(path));
    }

    public ForApiBuilder fromFile(URL path) throws ConfigurationLoadException {
      return from(configHelper.fromFile(path));
    }

    public ForApiBuilder fromFile() throws ConfigurationLoadException {
      return fromFile(com.google.api.ads.common.lib.utils.Builder.DEFAULT_CONFIGURATION_FILENAME);
    }

    /**
     * Reads properties from the provided {@link Configuration} object
     * <br><br>
     * Known properties:
     * <ul>
     * <li>clientLoginServerUrl - e.g. https://www.google.com </li>
     * </ul>
     * Understands the following properties suffixes:
     * <br><br>
     * <ul>
     * <li>clientLoginToken</li>
     * <li>email</li>
     * <li>password</li>
     * </ul><br>
     * For example, the AdWords ClientLoginToken can be read from:
     * <code>api.adwords.clientLoginToken</code>
     *
     * @param config the configuration
     * @return Builder populated from the Configuration
     */
    public ForApiBuilder from(Configuration config) {
      clientLoginTokens.clientLoginTokenOverride =
          config.getString(getPropertyKey("clientLoginToken"), null);
      clientLoginTokens.clientLogin.setUsername(config.getString(getPropertyKey("email"), null));
      clientLoginTokens.clientLogin.setPassword(config.getString(getPropertyKey("password"), null));
      String clientLoginServerUrl = config.getString("clientLoginServerUrl", null);
      if (!Strings.isNullOrEmpty(clientLoginServerUrl)) {
        clientLoginTokens.clientLoginServerUrl = clientLoginServerUrl;
        clientLoginTokens.clientLogin.setServerUrl(new GenericUrl(clientLoginServerUrl));
      }
      return this;
    }

    /**
     * Includes email & password to fetch client login token.
     */
    public ForApiBuilder withEmailAndPassword(String email, String password) {
      clientLoginTokens.clientLogin.setUsername(email);
      clientLoginTokens.clientLogin.setPassword(password);
      return this;
    }

    /**
     * Sets the {@link HttpTransport} to be used to make the request. By
     * default, {@link NetHttpTransport} will be used, but due to some
     * environment restrictions, you may want to use a different transport,
     * such as {@code UrlFetchTransport} for AppEngine.
     */
    public ForApiBuilder withHttpTransport(HttpTransport httpTransport) {
      clientLoginTokens.clientLogin.setTransport(httpTransport);
      return this;
    }

    /**
     * Sets the client login server that the token will be fetched typically in
     * the form of {@code https://www.google.com}
     */
    public ForApiBuilder withClientLoginServerUrl(String serverUrl) {
      clientLoginTokens.clientLoginServerUrl = serverUrl;
      clientLoginTokens.clientLogin.setServerUrl(new GenericUrl(serverUrl));
      return this;
    }

    /**
     * Validates the {@code ClientLoginTokens} object.
     * @throws ValidationException if the {@code ClientLoginTokens} did not
     *         validate
     */
    private void validate() throws ValidationException {
      if (!Strings.isNullOrEmpty(clientLoginTokens.clientLoginTokenOverride)
          && (!Strings.isNullOrEmpty(clientLoginTokens.clientLogin.getUsername())
              || !Strings.isNullOrEmpty(clientLoginTokens.clientLogin.getPassword()))) {
        throw new ValidationException(
            "Email and password or client login token should be set. Not both.", "");
      }

      if (Strings.isNullOrEmpty(clientLoginTokens.clientLoginTokenOverride)
          && (Strings.isNullOrEmpty(clientLoginTokens.clientLogin.getUsername())
              || Strings.isNullOrEmpty(clientLoginTokens.clientLogin.getPassword()))) {
        throw new ValidationException(
            "Email and password or client login token must be set.", "");
      }
    }

    public ClientLoginTokens build() throws ValidationException {
      validate();
      if (clientLoginTokens.clientLogin.getTransport() == null) {
        clientLoginTokens.clientLogin.setTransport(DEFAULT_HTTP_TRANSPORT);
      }
      clientLoginTokens.clientLogin.setAccountType("GOOGLE");
      clientLoginTokens.clientLogin.setAuthTokenType(api.getService());
      return clientLoginTokens;
    }

    /**
     * Adds the correct property key prefix to match the API.
     *
     * @param suffix the property suffix
     * @return property value for key
     */
    @VisibleForTesting
    String getPropertyKey(String suffix) {
      return api.getPropKeyPrefix() + suffix;
    }
  }
}
